﻿using System;
using System.Collections.Generic;
using System.Linq;

namespace Mathematics.Expressions
{
    public class Domain:MathExpression
    {
        #region Local Members

        private readonly Dictionary<string, FunctionDefinition> _functions = new Dictionary<string, FunctionDefinition>();
        private readonly Dictionary<string, Variable> _variables = new Dictionary<string, Variable>();
        private readonly List<MathExpression> _expressions = new List<MathExpression>();
        private readonly List<Domain> _domains = new List<Domain>();

        #endregion

        #region Constructors

        private Domain(Domain parentDomain,string name)
        {
            if (parentDomain == null)
                throw new ArgumentNullException("parentDomain");

            if (string.IsNullOrEmpty(name))
                name="_"+ (++_anonDomainCounter);
            
            ParentDomain = parentDomain;
            Name = name;
        }

        private Domain()
        {
            Name = "_";
        }

        static Domain()
        {
            ResetRoot();
        }

        #endregion

        #region Public Properties

        public string Name { get; private set; }

        public Domain ParentDomain { get; private set; }

        public int Depth
        {
            get
            {
                if (ParentDomain == null)
                    return 0;
                return ParentDomain.Depth + 1;
            }
        }

        #endregion

        #region Execution Methods

        public override bool CanEvaluate()
        {
            return _expressions.All(e => e.CanEvaluate());
        }

        public override ExpressionValue Evaluate()
        {
            var value = ExpressionValue.Void;
            foreach (var exp in _expressions)
            {
                var v = exp.Evaluate();
                if (v.HasValue)
                    value = v;
            }
            return value;
        }

        public void Reset()
        {
            ClearFunctions();
            ClearVariables();
            ClearExpression();
        }

        #endregion

        #region Expression Methods

        public void AddExpression(MathExpression expression)
        {
            _expressions.Add(expression);
        }

        public void ClearExpression()
        {
            _expressions.Clear();
        }

        #endregion

        #region Variable Methods

        public IEnumerable<Variable> Variables
        {
            get { return _variables.Values; }
        }

        public IEnumerable<Variable> AllVariables
        {
            get
            {
                foreach (var v in _variables.Values)
                    yield return v;

                if (ParentDomain != null)
                {
                    foreach (var v in ParentDomain.AllVariables)
                        yield return v;
                }
            }
        }

        public bool HasVariable(string variableName)
        {
            return _variables.Values.Any(v => v.Name == variableName);
        }

        public Variable GetVariable(string name)
        {
            return _variables.Values.SingleOrDefault(v => v.Name==name);
        }

        public Variable DefineVariable(string name)
        {
            var variable = new Variable(name);
            AddVariable(variable);
            return variable;
        }

        public Variable DefineVariable(string name, ExpressionValue value)
        {
            var variable = new Variable(name);
            variable.Set( value);
            AddVariable(variable);
            return variable;
        }

        public Variable DefineConstantVariable(string name, ExpressionValue value)
        {
            var variable = new Variable(name,false,value);
            AddVariable(variable);
            return variable;
        }

        private void AddVariable(Variable variable)
        {
            if (HasVariable(variable.Name))
                throw new DuplicateVariableException(string.Format("Variable {0} is already defined.", variable.Name));

            _variables.Add(variable.Name, variable);
        }

        public Variable SafeGetVariable(string name)
        {
            return (HasVariable(name))
                       ? GetVariable(name)
                       : DefineVariable(name);
        }

        public void ClearVariables()
        {
            _variables.Clear();
        }

        public void AssignVariable(string name, ExpressionValue value)
        {
            var variable = GetVariable(name);
            variable.Set(value);
        }

        public bool AllVariablesAssigned()
        {
            return _variables.Values.All(v => v.IsAssigned);
        }

        #endregion

        #region Function Methods

        public bool HasFunction(string functionName)
        {
            return _functions.Values.Any(v => v.Name == functionName);
        }

        public IEnumerable<FunctionDefinition> Functions
        {
            get { return _functions.Values; }
        }

        public IEnumerable<FunctionDefinition> AllFunctions
        {
            get
            {
                foreach (var v in _functions.Values)
                    yield return v;

                if (ParentDomain != null)
                {
                    foreach (var v in ParentDomain.AllFunctions)
                        yield return v;
                }
            }
        }

        public FunctionDefinition GetFunction(string name)
        {
            return _functions.Values.SingleOrDefault(f => f.Name == name);
        }

        public UserDefinedFunction DefineFunction(string name, MathExpression expression, params ParameterDefinition[] parameters)
        {
            var domain = new Domain(this,name);
            domain.AddExpression(expression);

            return DefineFunction(name, domain, parameters);
        }

        public UserDefinedFunction DefineFunction(string name, Domain functionDomain, params ParameterDefinition[] parameters)
        {
            var function = new UserDefinedFunction(name, functionDomain, parameters);
            foreach (var p in parameters)
                functionDomain.SafeGetVariable(p.Name);

            RegisterFunction(function);

            return function;
        }

        private void RegisterFunction(FunctionDefinition def)
        {
            _functions.Add(def.Name, def);
        }

        public void ClearFunctions()
        {
            _functions.Clear();
        }

        #endregion

        #region Domain Methods

        internal void AddDomain(Domain domain)
        {
            _domains.Add(domain);
        }

        public IEnumerable<Domain> Domains
        {
            get { return _domains; }
        }

        public IEnumerable<Domain> AllDomains
        {
            get
            {
                foreach (var domain in _domains)
                {
                    yield return domain;
                    foreach (var child in domain.AllDomains)
                        yield return child;
                }
            }
        }

        #endregion

        #region Static Members

        private static int _anonDomainCounter = 0;

        public static Domain RootDomain { get; private set; }

        public static Domain CurrentDomain { get; private set; }

        public static void ResetRoot()
        {
            _anonDomainCounter = 0;
            var newDomain = new Domain();

            // add built in functions
            AddBuiltInFunctions(newDomain);
            AddBuiltInVariables(newDomain);

            RootDomain = newDomain;
            CurrentDomain = RootDomain;
        }

        internal static Domain EnterDomain(string name)
        {
            var domain= new Domain(CurrentDomain,name);
            CurrentDomain.AddDomain(domain);
            CurrentDomain = domain;
            return domain;
        }

        internal static Domain ExitDomain()
        {
            var domain = CurrentDomain.ParentDomain;
            if (domain == null)
                throw new ApplicationAbortException();
            CurrentDomain = domain;
            return domain;
        }

        private static void AddBuiltInFunctions(Domain domain)
        {
            #region Trigonometric Functions

            domain.RegisterFunction(SystemFunctions.Sinr);
            domain.RegisterFunction(SystemFunctions.Sin);
            domain.RegisterFunction(SystemFunctions.Cosr);
            domain.RegisterFunction(SystemFunctions.Cos);
            domain.RegisterFunction(SystemFunctions.Tanr);
            domain.RegisterFunction(SystemFunctions.Tan);
            domain.RegisterFunction(SystemFunctions.Rad);
            domain.RegisterFunction(SystemFunctions.Deg);

            #endregion

            #region Data Type Functions

            domain.RegisterFunction(SystemFunctions.Type);
            domain.RegisterFunction(SystemFunctions.IsBool);
            domain.RegisterFunction(SystemFunctions.IsNumber);
            domain.RegisterFunction(SystemFunctions.IsString);
            domain.RegisterFunction(SystemFunctions.IsVoid);
            domain.RegisterFunction(SystemFunctions.IsTrue);
            domain.RegisterFunction(SystemFunctions.IsFalse);
            domain.RegisterFunction(SystemFunctions.ToBool);
            domain.RegisterFunction(SystemFunctions.ToNumber);
            domain.RegisterFunction(SystemFunctions.ToString);

            #endregion

            #region String Functions

            domain.RegisterFunction(SystemFunctions.Length);
            domain.RegisterFunction(SystemFunctions.Reverse);

            #endregion

            #region Introspection Functions

            domain.RegisterFunction(SystemFunctions.Domains);
            domain.RegisterFunction(SystemFunctions.DomainsL);
            domain.RegisterFunction(SystemFunctions.Functions);
            domain.RegisterFunction(SystemFunctions.FunctionsL);
            domain.RegisterFunction(SystemFunctions.Variables);
            domain.RegisterFunction(SystemFunctions.VariablesL);

            #endregion
        }

        private static void AddBuiltInVariables(Domain domain)
        {
            domain.DefineConstantVariable("pi", ExpressionValue.NewNumber(Math.PI));
            domain.DefineConstantVariable("e", ExpressionValue.NewNumber(Math.E));
        }

        #endregion

        protected internal override MathExpression Reduce(out bool didReduce)
        {
            didReduce = false;
            return this;
        }

    }
}
